package com.zenika.zentracker.hibernate.interceptor;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.CallbackException;
import org.hibernate.Interceptor;
import org.hibernate.collection.AbstractPersistentCollection;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.CollectionEntry;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.event.EventSource;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.type.Type;

import com.zenika.zentracker.hibernate.ZenTrackerEmptyInterceptor;
import com.zenika.zentracker.hibernate.info.CollectionActivity;
import com.zenika.zentracker.hibernate.info.CollectionInformation;
import com.zenika.zentracker.hibernate.info.CollectionKey;
import com.zenika.zentracker.hibernate.info.CollectionRecreate;
import com.zenika.zentracker.hibernate.info.CollectionRemove;
import com.zenika.zentracker.hibernate.info.CollectionUpdate;
import com.zenika.zentracker.hibernate.info.UpdatedObjectInfo;
import com.zenika.zentracker.hibernate.jdbc.SQLSniffer;
import com.zenika.zentracker.jmx.JMXSessionStats;
import com.zenika.zentracker.jmx.JMXSessionStatsImpl;

/**
 * ZenTracker Interceptor. This Interceptor is used to monitor in details the activity of a Hibernate Session.
 * 
 * Please note that this class is intended to have some State. It is <b>NOT</b> a stateless class. When used with
 * Hibernate a new Instance should be created for every Session. It should not be attached to the SessionFactory.
 * 
 * During the lifetime of a Session this interceptor will record some activity and at a transaction completion, this
 * activity will be logged.
 * 
 * If NOT using a Session in a transaction, than some code should be written to log manually the Interceptor information
 * before the object is destroyed.
 * 
 * This class logs :
 * <ul>
 * <li>The number of loaded objects by type</li>
 * <li>The number of created objects by type</li>
 * <li>The number of updated objects by type</li>
 * <li>The number of deleted objects by type</li>
 * <li>The exact updated collection (Added and Removed objects)</li>
 * <li>The execution time of every query, by query, and for each query the min, max and average execution time</li>
 * 
 * @author Zenika
 * 
 */
public class SessionStatisticsInterceptor extends ZenTrackerEmptyInterceptor {

    private static final long serialVersionUID = 1L;

    private Interceptor userDefinedInterceptor;
    
    /**
     * Map of all the updated objects. Key: Entity type / Value: Number of objects of this type updated
     */
    private Map updatedObjects = new HashMap();

    /**
     * Map of all the created objects. Key: Entity type / Value: Number of objects of this type created
     */
    private Map createdObjects = new HashMap();

    /**
     * Map of all the loaded objects. Key: Entity type / Value: Number of objects of this type loaded
     */
    private Map loadedObjects = new HashMap();

    /**
     * Map of all the deleted objects. Key: Entity type / Value: Number of objects of this type deleted
     */
    private Map deletedObjects = new HashMap();

    /**
     * Map of all the recorded activity on collections. Key: a CollectionKey (unique identifier of a Collection) Value:
     * a List of CollectionInformation (Events of removale, update and recreate)
     */
    private Map collectionActivity = new HashMap();
    
    private List executionTimes = new ArrayList();
    
    
    /**
     * Map of all the dirty objects with information on updated properties
     */
    private List dirtyObjects = new ArrayList();

    /**
     * Record the number of flush events
     */
    private int totalFlush = 0;

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onLoad(java.lang.Object, java.io.Serializable, java.lang.Object[],
     *      java.lang.String[], org.hibernate.type.Type[])
     */
    public boolean onLoad(
            final Object entity,
            final Serializable id,
            final Object[] state,
            final String[] propertyNames,
            final Type[] types) {
        completeEntityStatistics(loadedObjects, entity.getClass().getName());

        if (userDefinedInterceptor != null) {
            return userDefinedInterceptor.onLoad(entity, id, state, propertyNames, types);
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onDelete(java.lang.Object, java.io.Serializable, java.lang.Object[],
     *      java.lang.String[], org.hibernate.type.Type[])
     */
    public void onDelete(
            final Object entity,
            final Serializable id,
            final Object[] state,
            final String[] propertyNames,
            final Type[] types) {
        completeEntityStatistics(deletedObjects, entity.getClass().getName());

        if (userDefinedInterceptor != null) {
            userDefinedInterceptor.onDelete(entity, id, state, propertyNames, types);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onFlushDirty(java.lang.Object, java.io.Serializable, java.lang.Object[],
     *      java.lang.Object[], java.lang.String[], org.hibernate.type.Type[])
     */
    public boolean onFlushDirty(
            final Object entity,
            final Serializable id,
            final Object[] currentState,
            final Object[] previousState,
            final String[] propertyNames,
            final Type[] types) {
        completeEntityStatistics(updatedObjects, entity.getClass().getName());

        if (userDefinedInterceptor != null) {
            return userDefinedInterceptor.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onSave(java.lang.Object, java.io.Serializable, java.lang.Object[],
     *      java.lang.String[], org.hibernate.type.Type[])
     */
    public boolean onSave(
            final Object entity,
            final Serializable id,
            final Object[] state,
            final String[] propertyNames,
            final Type[] types) {
        completeEntityStatistics(createdObjects, entity.getClass().getName());

        if (userDefinedInterceptor != null) {
            return userDefinedInterceptor.onSave(entity, id, state, propertyNames, types);
        } else {
            return false;
        }

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#postFlush(java.util.Iterator)
     */
    public void postFlush(final Iterator entities) {
        totalFlush++;
        if (userDefinedInterceptor != null) {
            userDefinedInterceptor.postFlush(entities);    
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onCollectionUpdate(java.lang.Object, java.io.Serializable)
     */
    public void onCollectionUpdate(final Object collection, final Serializable key) throws CallbackException {
        if (collection instanceof PersistentCollection) {
            PersistentCollection persistentColl = (PersistentCollection) collection;

            final CollectionPersister persister = extractCollectionPersister(persistentColl);

            Serializable snapshot = persistentColl.getSnapshot(persister);
            Serializable storedSnapshot = persistentColl.getStoredSnapshot();

            CollectionKey collKey = buildCollectionKey(key, persistentColl);
            CollectionUpdate information = new CollectionUpdate(totalFlush + 1, storedSnapshot,snapshot);

            completeCollectionStatistics(collectionActivity, collKey, information);
        }
        
        if (userDefinedInterceptor != null) {
            userDefinedInterceptor.onCollectionUpdate(collection, key);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onCollectionRemove(java.lang.Object, java.io.Serializable)
     */
    public void onCollectionRemove(final Object collection, final Serializable key) throws CallbackException {
        if (collection instanceof PersistentCollection) {
            PersistentCollection persistentColl = (PersistentCollection) collection;

            final CollectionPersister persister = extractCollectionPersister(persistentColl);

            Serializable snapshot = persistentColl.getSnapshot(persister);

            CollectionKey collKey = buildCollectionKey(key, persistentColl);
            CollectionRemove information = new CollectionRemove(totalFlush + 1, snapshot);

            completeCollectionStatistics(collectionActivity, collKey, information);
        }
        
        if (userDefinedInterceptor != null) {
            userDefinedInterceptor.onCollectionRemove(collection, key);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.hibernate.EmptyInterceptor#onCollectionRecreate(java.lang.Object, java.io.Serializable)
     */
    public void onCollectionRecreate(final Object collection, final Serializable key) throws CallbackException {
        if (collection instanceof PersistentCollection) {
            PersistentCollection persistentColl = (PersistentCollection) collection;

            Serializable storedSnapshot = persistentColl.getStoredSnapshot();

            CollectionKey collKey = buildCollectionKey(key, persistentColl);
            CollectionRecreate information = new CollectionRecreate(totalFlush + 1, storedSnapshot);

            completeCollectionStatistics(collectionActivity, collKey, information);
        }
        
        if (userDefinedInterceptor != null) {
            userDefinedInterceptor.onCollectionRecreate(collection, key);
        }
    }

    /**
     * Build from a Hibernate key and a PersistentCollection a unique identifier, a CollectionKey object.
     * 
     * Sometimes, like in the case of a CollectionRecreate event, the role attribute of the collection is null. In this
     * case, by introspection, we check on the owner object to which collection it depends to calculate the role
     * attribute.
     * 
     * @param key Hibernate collection key
     * @param persistentColl
     * @return Collection Key, the ZenTracker collection identifier
     */
    private CollectionKey buildCollectionKey(Serializable key, PersistentCollection persistentColl) {
        String role = persistentColl.getRole();
        Object owner = persistentColl.getOwner();

        if (role != null) {
            return new CollectionKey(key, role, owner);
        }

        String calculatedRole = null;
        try {
            Class collectionClass = Class.forName(Collection.class.getName());
            Class ownerClass = Class.forName(owner.getClass().getName());
            Field f[] = ownerClass.getDeclaredFields();
            for (int i = 0; i < f.length; i++) {
                Field field = f[i];
                field.setAccessible(true);
                Object fieldValue = field.get(owner);
                String fieldName = field.getName();
                field.setAccessible(false);
                if (collectionClass.isInstance(fieldValue)) {
                    if (persistentColl == fieldValue) {
                        calculatedRole = owner.getClass().getName() + "." + fieldName;
                        break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new CollectionKey(key, calculatedRole, owner);
    }

    public void addUpdatedObjectInfo(UpdatedObjectInfo objectInfo) {
       dirtyObjects.add(objectInfo);
    }

    /**
     * Method dumping all the information stored in the Interceptor into the logs.
     */
    public void logSummary() {
        logCRUDEntityInformation(loadedObjects, "loaded");
        logCRUDEntityInformation(updatedObjects, "updated");
        logCRUDEntityInformation(createdObjects, "created");
        logCRUDEntityInformation(deletedObjects, "deleted");
        logCollectionInformation(collectionActivity);
        logUpdatedObjectsInformation(dirtyObjects);
        logExecutionTimes();
    }

    private void logUpdatedObjectsInformation(List dirtyObjectsInfo2) {
        Iterator iterator = dirtyObjects.iterator();
        while (iterator.hasNext()) {
            UpdatedObjectInfo dirtyObject = (UpdatedObjectInfo) iterator.next();

            getLog().info(dirtyObject.toString());
        }
    }
    
    private void logExecutionTimes() {
        List times = getExecutionTimes();
        for (int i = 0; i < times.size(); i++) {
            getLog().info(times.get(i));
        }
    }

    /**
     * Retrieve the CollectionPersister from a PersistentCollection.
     * 
     * @param persistentColl The persistent collection.
     * @return The CollectionPersister
     */
    private CollectionPersister extractCollectionPersister(PersistentCollection persistentColl) {

        AbstractPersistentCollection abstractPersistentCollection = (AbstractPersistentCollection) persistentColl;

        PersistenceContext persistenceContext = null;
        // Hack to be compatible starting from Hibernate 3.1.
        // In version 3.1 method getSession() is protected and not public
        try {
            Class ownerClass = Class.forName(AbstractPersistentCollection.class.getName());
            Method getSessionMethod = ownerClass.getDeclaredMethod("getSession", new Class[0]);
            getSessionMethod.setAccessible(true);
            Object getSessionResult = getSessionMethod.invoke(abstractPersistentCollection, new Object[0]);
            getSessionMethod.setAccessible(false);
            EventSource session = (EventSource) getSessionResult;
            persistenceContext = session.getPersistenceContext();
        } catch (Exception e) {
            e.printStackTrace();
        }

        CollectionEntry entry = persistenceContext.getCollectionEntryOrNull(persistentColl);
        final CollectionPersister roleBeforeFlush = (entry == null) ? null : entry.getLoadedPersister();
        return roleBeforeFlush;
    }

    /**
     * Log CRUD information for an Entity
     * 
     * @param entities Map of entities. Key: Entity name, Value: Number of times for a CRUD operation.
     * @param status
     */
    private void logCRUDEntityInformation(Map entities, String status) {
        Set keySet = entities.keySet();
        for (Iterator iterator = keySet.iterator(); iterator.hasNext();) {
            String entityName = (String) iterator.next();
            StringBuffer buffer = new StringBuffer();
            buffer.append("Entity <").append(entityName).append("> ");
            buffer.append(status).append(" ").append(entities.get(entityName)).append(" times");
            getLog().info(buffer.toString());
        }
    }

    /**
     * Log all the activity that happened on collection.
     * 
     * @param collectionsActivity Key, a CollectionKey object. Value, list of CollectionInformation objects.
     */
    private void logCollectionInformation(Map collectionsActivity) {
        Iterator iterator = collectionsActivity.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry keyValue = (Map.Entry) iterator.next();
            CollectionKey key = (CollectionKey) keyValue.getKey();
            CollectionActivity activities = (CollectionActivity) keyValue.getValue();

            Iterator iter = activities.collectionInfos().iterator();
            while (iter.hasNext()) {
                getLog().info(key + " has been modified ! " + (CollectionInformation) iter.next());
            }
        }
    }

    /**
     * Increment the statistics on an Entity.
     * 
     * @param entities A CRUD Map. Key, the Entity class. Value, the hits on an entity.
     * @param entity The class of the Entity to increment
     */
    private void completeEntityStatistics(Map entities, String entityName) {
        Long quantity = (Long) entities.get(entityName);
        if (quantity == null) {
            entities.put(entityName, new Long(1));
        } else {
            entities.put(entityName, new Long(quantity.longValue() + 1));
        }
    }

    /**
     * Adds a CollectionInformation for a CollectionKey. Allows to group all the events happening on a collection.
     * 
     * @param collections
     * @param key
     * @param information
     */
    private void completeCollectionStatistics(Map collections, CollectionKey key, CollectionInformation information) {
        CollectionActivity activity = (CollectionActivity) collections.get(key);
        if (activity == null) {
            collections.put(key, new CollectionActivity(information));
        } else {
            activity.add(information);
        }
    }

    // *************** GETTERS **********************
    public Map getUpdatedObjects() {
        return updatedObjects;
    }

    public Map getCreatedObjects() {
        return createdObjects;
    }

    public Map getLoadedObjects() {
        return loadedObjects;
    }

    public Map getDeletedObjects() {
        return deletedObjects;
    }

    public Map getCollectionActivity() {
        return collectionActivity;
    }

    public int getTotalFlush() {
        return totalFlush;
    }

    public List getDirtyObjects() {
        return dirtyObjects;
    }

    public List getExecutionTimes() {
        if (executionTimes.isEmpty()) {
            executionTimes.addAll(SQLSniffer.getInstance().logAndDumpSqlExecutionInformation(this));
        }
        return executionTimes;
    }
    
    public JMXSessionStats buildJMXStats() {
        return new JMXSessionStatsImpl(this);
    }

    public void setUserDefinedInterceptor(Interceptor userDefinedInterceptor) {
        this.userDefinedInterceptor = userDefinedInterceptor;
    }

}